import { Bi } from "@/module-00-codebase/pkg-01-container";
import { Throwable } from "@/module-00-codebase/pkg-02-tool";

export type DataType = "bool" | "date" | "dict" | "oref" | "nmbr" | "pswd" | "txea" | "text";
export interface Field {
  label: string;
  singularLabel: string | null;
  name: string;
  type: DataType;
  placeholder: string;
  disabled: boolean;
  mode: FieldModeI;
  getter: (refer: any) => any;
  setter: (refer: any, value: any) => void;
  serialize: (refer: any) => string;
  contains: (refer: any) => boolean;
  validate: (refer: any) => Promise<string>;
  assign: (key: string, value: any) => Field;
  clone: () => Field;
  izArray: boolean;
  isSingleNameOref: boolean;
  orefType?: string;
  declaringType: string;
  orefOptions?: any[];
  dictGroupName: string | null;
  length: number;
}
export interface FieldModeI {
  readOnly: boolean;
  nullable: boolean; // cannot be null if false
  insertable: boolean; // invail when post if false
  updatable: boolean; // invail once post if false
  cascadable: boolean; // invail it has been not null if false
  validations: string[]; // validation definitions
}
export class PropertyField implements Field {
  constructor(
    public label: string, //e
    public name: string,
    public type: DataType = "text",
    public placeholder: string = label,
    public disabled: boolean = false,
    public mode = new PropertyFieldMode(),
    public getter = DEFAULT_GETTER,
    public setter = DEFAULT_SETTER,
    public serialize = DEFAULT_SERIALIZE,
    public validate = DEFAULT_VALIDATE,
    public contains = DEFAULT_CONTAINS,
    public clone = DEFAULT_CLONE,
    public assign = DEFAULT_ASIGN,
    public izArray = false,
    public isSingleNameOref = false,
    public orefType?: string,
    public declaringType: string = "",
    public orefOptions?: any[],
    public dictGroupName = null,
    public length = 32,
    public singularLabel: string | null = null
  ) {}
}
export class PropertyFieldMode implements FieldModeI {
  constructor(public readOnly = false, public nullable = true, public insertable = true, public updatable = true, public cascadable = false, public validations = []) {}
}
export function DEFAULT_GETTER(this: Field, refer: any) {
  return Bi.utils.readValue(refer, this.name);
}
export function DEFAULT_SETTER(this: Field, refer: any, value: any) {
  let splits = Bi.utils.splitProperties(this.name);
  if (splits.length > 0) {
    for (var i = 0; i < splits.length - 1; i++) {
      let finalPathOfProperty = splits[i];
      let tempRefer = refer[splits[i]];
      if (tempRefer === undefined) {
        refer[finalPathOfProperty] = tempRefer = (finalPathOfProperty as any) % 1 === 0 ? [] : {};
      }
      refer = tempRefer;
    }
    if (this.type === "nmbr" && typeof value !== "number") {
      if (typeof value === "string" && /[[0-9]+(.[0-9]+)?/.test(value)) {
        value = Number.parseFloat(value);
      } else {
        value = null;
      }
    }
    if (this.type === "date") {
      if (Bi.utils.isNullOrEmpty(value)) {
        value = null;
      } else if (value instanceof Date) {
        value = (value as Date).getTime();
      } else if (typeof value === "string") {
        value = Bi.dateFormat.parse(value);
        value = !!value ? (value as Date).getTime() : null;
      }
    }
    refer[splits.slice(-1)[0]] = value;
    if (this.type === "pswd" && !value) {
      delete refer[splits.slice(-1)[0]];
    }
  } else if (!this.izArray) {
    const availableAttrs = Bi.propertyDescription.getProperties(this.orefType, typeof refer.id === "string" && refer.id.indexOf("tempid_") !== 0 ? "edit" : "add") as string[];
    availableAttrs.forEach(k => (refer[k] = value[k]));
  } else {
    refer.length = 0;
    Array.prototype.push.apply(refer, value);
  }
}
export function DEFAULT_CLONE(this: Field): Field {
  return Bi.utils.deepCopy(this);
}
export function DEFAULT_ASIGN(this: Field, key: string, value: any): Field {
  Bi.utils.writeValue(this, key, value);
  return this;
}
export function DEFAULT_SERIALIZE(this: Field, refer: any): any {
  let value = this.getter(refer);
  if (value === undefined || value === null) {
    return "";
  } else if (this.izArray === true) {
    if (!Array.isArray(value)) {
      this.setter(refer, []);
      return "";
    } else {
      const componentField = Object.assign(this.clone(), { izArray: false });
      const parentName = this.name;
      return (value as any[]).map((v, i) => Object.assign(componentField, { name: `${parentName}.${i}` }).serialize(refer)).join(" / ");
    }
  } else if (this.type === "date") {
    if (typeof value === "number" && value > 0) {
      return Bi.dateFormat.format(new Date(value));
    } else {
      return "";
    }
  } else if (this.type === "bool") {
    return value ? "Yes" : "No";
  } else if (value === undefined || value === null) {
    return "";
  } else if (this.type === "nmbr" && isNaN(value)) {
    return "";
  } else if (this.type === "oref") {
    if (!this.orefType) return Throwable("Impossible Error: No orefType");
    let nameField: Field = Bi.utils.getNameField(this.orefType);
    let nodeField: Field | undefined = Bi.utils.getNodeField(this.orefType);
    let nodeValue: any;
    if (nodeField === undefined || !(nodeValue = nodeField.getter(value))) {
      return nameField.serialize(value);
    } else {
      return nameField.serialize(value) + ", " + nameField.serialize(nodeValue);
    }
  } else if (this.type === "dict") {
    if (!this.dictGroupName) {
      return Throwable(`dict type ${this.declaringType}.${this.name} doesn't have a dictGroupName`);
    } else {
      return Bi.dict.get(this.dictGroupName, value);
    }
  } else {
    return value;
  }
}
export async function DEFAULT_VALIDATE(this: Field, refer: any) {
  return fieldValidate(this, refer, new Set());
}
export function DEFAULT_CONTAINS(this: Field, refer: any) {
  const properties = Bi.utils.splitProperties(this.name) as string[];
  const parentProperties = properties.slice(0, -1);
  const lastProperty = properties.slice(-1)[0];
  const parent = parentProperties.reduce((res, temp) => (res || {})[temp], refer);
  return Object.getOwnPropertyNames(parent || {}).includes(lastProperty);
}
export interface PropertyGroup {
  labels: string[];
  indexes: number[];
  orefType: string;
}
const fieldValidate = async (field: Field, refer: any, circularCache: Set<any>) => {
  let message = "";
  try {
    const currentValue = field.getter(refer);
    if (!isBaseType(currentValue) && isCachedAndCacheIfNot(circularCache, currentValue)) {
      // skip
    } else if (!!field.mode.readOnly) {
      // skip
    } else if (!!(message = await simpleValidate(field, refer))) {
      // skip
    } else if (!field.orefType) {
      //skip
    } else if (currentValue === null || currentValue === undefined) {
      // skip
    } else if (field.izArray) {
      if (!Array.isArray(currentValue)) {
        message = `${field.name} should be an array but a ${typeof currentValue}`;
      } else {
        for (let i = 0; i < currentValue.length; i++) {
          const nextField = Bi.propertyDescription.get(field.declaringType, `${field.name}.${i}`) as Field;
          if (currentValue[i] === null || currentValue[i] === undefined) continue;
          if (!!(message = await validateObj(nextField, refer, circularCache))) break;
        }
      }
    } else {
      if (Array.isArray(currentValue)) {
        message = `${field.name} should be an object but an array`;
      } else {
        message = await validateObj(field, refer, circularCache);
      }
    }
  } catch ( ex:unknown ) {
 const e = ex instanceof Error?ex:new Error(JSON.stringify(ex));
    message = e.message;
  }
  return message;
};
const validateObj = async (field: Field, refer: any, circularCache: Set<any>) => {
  let message: any = "";
  try {
    const nextFields = Bi.propertyDescription.get(field.orefType) as Field[];
    const parent = field.getter(refer);
    const isEdit = !!parent.id && parent.id.indexOf("tempid_") !== 0;
    const opable = isEdit ? "updatable" : "insertable";
    const parentKeys = Object.keys(parent);
    const keys = nextFields
      .filter(f => f.mode[opable])
      .map(f => f.name)
      .filter(name => parentKeys.includes(name))
      .sort((a, b) => a.localeCompare(b));
    for (let i = 0; i < keys.length; i++) {
      const nextField = Bi.propertyDescription.get(field.declaringType, `${field.name}.${keys[i]}`) as Field;
      if (!!(message = await fieldValidate(nextField, refer, circularCache))) break;
    }
  } catch ( ex:unknown ) {
 const e = ex instanceof Error?ex:new Error(JSON.stringify(ex));
    message = e;
  } finally {
    return message;
  }
};
const simpleValidate = async (field: Field, refer: any) => {
  let message = "";
  try {
    const valis = field.mode.validations.sort(VALIDATION_SORT);
    for (let i = 0; i < valis.length; i++) if (!!(message = await Bi.validator.validate(valis[i], field, refer))) break;
  } catch ( ex:unknown ) {
 const e = ex instanceof Error?ex:new Error(JSON.stringify(ex));
    message = e.message;
  }
  return message;
};
const isBaseType = (val: any) => {
  return typeof val !== "object" || val === null;
};
const isCachedAndCacheIfNot = (set: Set<any>, val: any) => {
  if (set.has(val)) {
    return true;
  } else {
    set.add(val);
    return false;
  }
};
const valiTypes = ["notNull", "minNmbr", "maxNmbr", "minLen", "maxLen", "minDate", "maxDate", "minSize", "maxSize", "email", "pswd", "confirmPswd", "cron", "unique"];
export const VALIDATION_SORT = (a: string, b: string): number => {
  return valiTypes.findIndex(x => x === a.split("(")[0]) - valiTypes.findIndex(x => x === b.split("(")[0]);
};
export const SPLITER = ".";
